package agent.strategy;

import oracle.MoveTypes;
import oracle.Oracle;
import agent.Agent;

/**
 * The StrategyManager class holds all of the strategies that a given agent might want to consult.
 * The SM will take in various weights that help to decide when to listen to which strategy and for how long.
 * 
 * @author yutao
 *
 */
public class StrategyManager
{
	// The strategies we're going to be consulting
	/** The roundOneStrategy will probably be renamed the ExploreStrategy */
	roundOneStrategy exploreStrategy;
	/** The terminal strategy has blinders on and tries to immediately seek out the nearest terminal */
	TerminalStrategy terminalStrategy;
	ExploreNearestTerminal entStrategy;
	/** The terminal strategy is a greedy strategy that just looks for immediate reward and doesn't consider the long term*/
	GreedyStrategy greedyStrategy;
	
	/**
	 * Once we choose a strategy, we might commit to it for a certain period of time, so we need to know which
	 * strategy it is that we are sticking to.
	 */
	Strategy currentStrategy;
	/** 
	 * Whenever we start to listen a particular strategy, we plan an amount of time we think we want to continue
	 * listening to it. Obviously, this can be overridden if we discover important enough information, but if nothing
	 * new pops up, we'll stick with the same strategy for this number of moves.
	 */
	int strategyCommitment;
	
	/** This is the current Agent that we're advising */
	private Agent agent;
	
	/**
	 * Construct a new StrategyManager
	 */
	public StrategyManager()
	{
		this.strategyCommitment = 0;
	}
	
	/**
	 * This method defines the strategies we're using with the current agent
	 */
	public void defineStrategies()
	{
		if (this.agent == null)
		{
			System.err.println("We cannot define our strategies until you call StrategyManager.setAgent");
			System.exit(0);
		}
		
		this.exploreStrategy = new roundOneStrategy(this.agent.getBoardMemory());
		this.exploreStrategy.setAgent(this.agent);
		this.terminalStrategy = new TerminalStrategy(this.agent.getBoardMemory());
		this.terminalStrategy.setAgent(this.agent);
		this.greedyStrategy = new GreedyStrategy(this.agent.getBoardMemory());
		this.greedyStrategy.setAgent(this.agent);
		this.entStrategy = new ExploreNearestTerminal(this.agent.getBoardMemory());
		this.entStrategy.setAgent(this.agent);
	}
	
	/**
	 * Weigh the different strategies we consider and then return a recommended course of action according to
	 * the MoveTypes code
	 * 
	 * @return The move that we think is best
	 */
	public int recommendMove()
	{
		if (agent == null)
		{
			System.err.println("You must first initialize the StrategyManager with an agent");
			System.exit(0); // We can only recommend a move if we have knowledge from an agent
		}
		
		nextDirObject recommendedMove = null;
		
		// For now, hack in a forced look around at the start of each round
		if (this.agent.oracle.getCurrMoveNumber() == 0)
		{
			return MoveTypes.LOOK_AROUND;
		}
		
		int commitTime = this.agent.oracle.getNumMovesLeftInRound();
		int flag = 1; // The 1 means we want to use what we consider to be an accurate belief state
		nextDirObject recommendedGreedyMove = this.greedyStrategy.nextMove(commitTime, 0, flag);
		nextDirObject recommendedTerminalMove = this.terminalStrategy.nextMove(commitTime, 0, flag);
		
		/*if (this.agent.oracle.getRoundNumber() > 1)
		{
			int entCommitTime = 10;
			nextDirObject entMove = this.entStrategy.expectedUtil(entCommitTime);
			if (this.entStrategy.expectedUtil(0).probSuccess > 0)
			{
				return MoveTypes.LOOK_AROUND;
			}
			double distance = Math.min(entStrategy.getOldTerminalDistance() / .9, this.agent.oracle.getMaxSightRange()  / .9 + entMove.expectedUtility);
			double estimatedCost = distance * this.agent.getBoardMemory().getAverageObserved(this.agent.currentRound, true, true);
			double estimatedUtil = entStrategy.estimatePastTerminal(this.agent.oracle.getNumMovesLeftInRound(), distance, entCommitTime);
			
			System.out.println("Distance: " + distance);
			System.out.println("EstimatedCost: " + estimatedCost);
			System.out.println("EstimatedUtil: " + estimatedUtil);
			System.out.println("Suicide cost: " + this.agent.oracle.KILL_SELF_PENALTY);
			
			if (this.agent.currentRun < 5)
			{
				if (estimatedUtil + estimatedCost > (this.agent.oracle.KILL_SELF_PENALTY * 2))
				{
					recommendedMove = entMove;
				}
			}
		}*/
		
		int numWillExpose = this.greedyStrategy.numWillExpose(this.agent.currentPosition.row, this.agent.currentPosition.row);
		if (numWillExpose >= (this.agent.oracle.getMaxSightRange() * this.agent.oracle.getMaxSightRange()) * 6)
		{
			return MoveTypes.LOOK_AROUND;
		}
		
		recommendedMove = recommendedGreedyMove;
		
		this.strategyCommitment = this.agent.oracle.getNumMovesLeftInRound();
		
		// Be sure to take into account how committed we want to be and how much we want to weight
		// the current commitment
		
		if(Math.abs(recommendedGreedyMove.expectedUtility - Oracle.KILL_SELF_PENALTY) < .5)
			recommendedMove.moveDirection = MoveTypes.KILL_SELF;
		
		if(greedyStrategy.shouldExplore(this.agent.currentPosition.row, this.agent.currentPosition.column) && this.agent.currentRun < 6)
			recommendedMove.moveDirection = MoveTypes.LOOK_AROUND;
		int startCol = this.agent.currentPosition.column;
		int startRow = this.agent.currentPosition.row;

		if(!(startRow+1 >= Oracle.MAX_ROW || greedyStrategy.walls[startRow+1][startCol]) && greedyStrategy.terminals[startRow+1][startCol]){
			 recommendedMove.moveDirection = MoveTypes.DOWN;   
		}else if(!(startRow-1 < 0 || greedyStrategy.walls[startRow-1][startCol]) && greedyStrategy.terminals[startRow-1][startCol]){
			 recommendedMove.moveDirection = MoveTypes.DOWN;   
		}
		if(!(startCol-1 < 0 || greedyStrategy.walls[startRow][startCol-1]) && greedyStrategy.terminals[startRow][startCol-1]){
		    recommendedMove.moveDirection = MoveTypes.LEFT; 
		}
		if(!(startCol+1 >= Oracle.MAX_COL || greedyStrategy.walls[startRow][startCol+1]) && greedyStrategy.terminals[startRow][startCol+1]){
		    recommendedMove.moveDirection = MoveTypes.RIGHT;                                   
		}
		
		return recommendedMove.moveDirection; // We'll just commit suicide until we work out exactly what to call
	}
	
	/**
	 * @return The agent that this StrategyManager is advising
	 */
	public Agent getAgent()
	{
		return this.agent;
	}
	
	/**
	 * @return Our current commitment to the current strategy
	 */
	public int getCommitment()
	{
		return this.strategyCommitment;
	}
	
	/**
	 * In order to have some amount of information about the world, we need to know what the agent knows.
	 * Additionally, when setting the agent, we will redefine the strategies to use the same size board as
	 * what our agent has.
	 * 
	 * @param a The agent we're advising
	 */
	public void setAgent(Agent a)
	{
		this.agent = a;
	}
}